package com.ccx.demo.config;

import com.ccx.demo.business.user.entity.TabUser;
import com.ccx.demo.config.init.AppConfig;
import com.ccx.demo.open.auth.cache.TokenCache;
import com.support.config.security.AuthHandler;
import com.support.config.security.SimpleAuthAdapter;
import com.support.mvc.entity.IOpenUser;
import com.support.mvc.entity.IUser;
import com.utils.exception.CodeException;
import com.utils.util.Rsa;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
import java.util.regex.Pattern;

/**
 * Spring Security 权限控制
 * <pre>
 *     中文文档：https://springcloud.cc/spring-security-zhcn.html
 *     官方文档：https://docs.spring.io/spring-security/site/docs/current/reference/html5/
 *             https://docs.spring.io/spring-security/site/docs/current/guides/helloworld-boot.html
 *
 *
 * @author 谢长春 2017年7月7日 下午1:28:31
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
// 使用 @EnableGlobalMethodSecurity 注解来启用基于注解的安全性
// @EnableGlobalMethodSecurity(securedEnabled = true) // 启用注解：@Secured；[@Secured("ROLE_USER"), @Secured("IS_AUTHENTICATED_ANONYMOUSLY")]
// @EnableGlobalMethodSecurity(prePostEnabled = true) // 启用注解：@PreAuthorize；[@PreAuthorize("hasAuthority('ROLE_USER')"), @PreAuthorize("isAnonymous()")]
@Slf4j
public class SecurityConfiguration {
//    /**
//     * 自定义校验规则
//     */
//    @Configuration
//    public static class GlobalMethodSecurity extends GlobalMethodSecurityConfiguration {
//        @Override
//        protected MethodSecurityExpressionHandler createExpressionHandler() {
//            final DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
//            expressionHandler.setPermissionEvaluator(null);
//            return expressionHandler;
//        }
//    }

    private static final Pattern REG = Pattern.compile("^[0-9a-f]+$");

    private static boolean tokenRegexp(final String token) {
        return REG.matcher(token).matches();
    }

    /**
     * 跨域配置。
     * 注意 allowedHeaders 会影响跨域条件
     * org.springframework.web.cors.DefaultCorsProcessor#handleInternal(ServerHttpRequest, ServerHttpResponse, CorsConfiguration, boolean)
     *
     * @return CorsConfigurationSource
     */
    @ConditionalOnProperty(value = "spring.app.corsEnabled", havingValue = "true")
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        final List<String> allowedHeaders = Arrays.asList(
                "content-type"
                , "x-token"
                , "x-version"
                , "X-Trace-Id"
        );
        final CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(Collections.singletonList("*"));
//        configuration.setAllowedOrigins(Arrays.asList("http://localhost:9528"));
        configuration.setAllowedMethods(Collections.singletonList("*"));
//        configuration.setAllowedMethods(Arrays.asList("GET","POST"));
        configuration.setAllowedHeaders(allowedHeaders);
        configuration.setExposedHeaders(allowedHeaders);
        configuration.setAllowCredentials(true);
        configuration.setMaxAge(3600L);
//        configuration.setMaxAge(10L);

        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    /**
     * 静态资源，无需登录授权
     *
     * @author 谢长春 2017年7月7日 上午9:58:58
     */
    @Order(1)
    @RequiredArgsConstructor
    @Configuration(proxyBeanMethods = false)
    public static class StaticWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
        private final AppConfig appConfig;

        @Override
        public void configure(WebSecurity web) {
            // 解决静态资源被拦截的问题
            web.ignoring().antMatchers( // resources 下的静态资源
                    "/favicon.ico"
                    , String.format("/%s/**", appConfig.getName())
                    , "/static/**"
                    , "/druid/**"
            );
            if (appConfig.isSwaggerEnabled()) {
                // knife4j 增强 swagger 页面
                web.ignoring().antMatchers(
                        "/doc.html"
                        , "/webjars/**"
                        , "/swagger-resources/**"
                        , "/v2/api-docs/**"
                        , "/v3/api-docs/**"
                );
            }
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .requestMatchers(configurer -> {
                        configurer
                                .antMatchers("/")
                                .antMatchers("/version")
                                // 健康检查接口
                                .antMatchers("/actuator/health/**")
                                // druid 监控
                                .antMatchers("/druid/**")
                                // resources 下的静态资源
                                .antMatchers(
                                        "/favicon.ico",
                                        String.format("/%s/**", appConfig.getName()),
                                        "/static/**"
                                );
                        if (appConfig.isSwaggerEnabled()) {
                            // knife4j 增强 swagger 页面
                            configurer.antMatchers(
                                    "/doc.html"
                                    , "/webjars/**"
                                    , "/swagger-resources/**"
                                    , "/v2/api-docs/**"
                                    , "/v3/api-docs/**"
                            );
                        }
                    })
                    .cors(Customizer.withDefaults()) // 跨域配置
                    // csrf 令牌 <<<<<<
                    .csrf(csrf -> {
                        csrf.disable();
//                        if (appConfig.isDev()) {
//                            csrf.disable();
//                        } else {
//                            csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and();
//                        }
                    })
                    .headers(headers -> headers
                            .addHeaderWriter((req, res) -> res.addHeader("X-Trace-Id", MDC.get("traceId")))
                            .xssProtection()
                    )
                    .anonymous()
                    .and().servletApi()
                    .and().headers()
                    .and().authorizeRequests()
                    .antMatchers(HttpMethod.TRACE, "/**").denyAll()
                    .anyRequest().permitAll()
                    .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)
            ;
        }
    }

    /**
     * 开放接口，无需登录授权
     *
     * @author 谢长春 2017年7月7日 上午9:58:58
     */
    @Order(2)
    @RequiredArgsConstructor
    @Configuration(proxyBeanMethods = false)
    public static class OpenWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
        private final AppConfig appConfig;

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .requestMatchers(configurer -> {
                        configurer
                                // open 前缀的 url 不需要登录
                                .antMatchers("/open/**")
                        ;
                    })
                    .cors(Customizer.withDefaults()) // 跨域配置
                    // csrf 令牌 <<<<<<
                    .csrf(csrf -> {
                        csrf.disable();
//                        if (appConfig.isDev()) {
//                            csrf.disable();
//                        } else {
//                            csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and();
//                        }
                    })
                    .headers(headers -> headers
                            .addHeaderWriter((req, res) -> res.addHeader("X-Trace-Id", MDC.get("traceId")))
                            .xssProtection()
                    )
                    .anonymous()
                    .and().servletApi()
                    .and().headers()
                    .and().authorizeRequests()
                    .antMatchers(HttpMethod.TRACE, "/**").denyAll()
                    .anyRequest().permitAll()
                    .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)
            ;
            final String anonymousKey = UUID.randomUUID().toString().replace("-", "");
            http.addFilterBefore(
                    // 自定义游客认证逻辑，如果用户已登录，访问开放接口时，使用会话中的用户。
                    // 当使用 token 模式登录是，需要在 controller 主动调用 OpenUser.fillUserByToken() 方法换用户信息
                    new AnonymousAuthenticationFilter(anonymousKey) {
                        public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
                            buildOpenUser((HttpServletRequest) req);
                            chain.doFilter(req, res);
                        }

                        /**
                         * 构造游客权限
                         *
                         * @param request {@link HttpServletRequest}
                         */
                        private void buildOpenUser(final HttpServletRequest request) {
                            final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
                            if (Objects.nonNull(authentication)) {
                                if ((authentication.getPrincipal() instanceof IUser)) {
                                    // 已登录用户，不需要处理
                                    return;
//                                } else if ((authentication.getPrincipal() instanceof IOpenUser.OpenUser)) {
//                                    // 已生成游客对象
//                                    return;
                                }
                            }
                            String token = request.getHeader("x-token");
                            if (StringUtils.isNotBlank(token)) {
                                if (!tokenRegexp(token)) {
                                    log.error("token格式错误:{}", token);
                                    token = null;
                                }
                            }
                            // 生成默认的游客对象， controller 有需求时使用 token 换用户信息，如果 token 为空则表示用户未登录
                            // 如果 token 过期，填充用户时捕获异常，避免接口报错，因为开放接口允许任何用户访问
                            final IOpenUser.OpenUser openUser = new IOpenUser.OpenUser(token, base64Token -> TokenCache.auth(base64Token).getUser());
                            final AnonymousAuthenticationToken authenticationToken = new AnonymousAuthenticationToken(
                                    anonymousKey,
                                    openUser,
                                    // 开放接口强制使用游客权限，避免开放接口注入权限，导致越权访问
                                    AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")
                            );
//                            authenticationToken.setDetails(authenticationDetailsSource.buildDetails(request));
                            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                        }
                    },
                    AnonymousAuthenticationFilter.class
            );
        }
    }

    /**
     * 封闭接口，需登录授权
     *
     * @author 谢长春 2017年7月7日 上午9:59:45
     */
    @RequiredArgsConstructor
    @Configuration(proxyBeanMethods = false)
    public static class FormLoginWebSecurityConfigurerAdapter extends SimpleAuthAdapter {
        private final AppConfig appConfig;

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            final AuthHandler authHandler = new AuthHandler();
            http
                    .cors(Customizer.withDefaults()) // 跨域配置
                    .csrf(csrf -> {
                        csrf.disable();
//                        if (appConfig.isDev()) {
//                            csrf.disable();
//                        } else {
//                            csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and();
//                        }
                        // csrf.ignoringAntMatchers("/druid/*");
                    })
                    // http 响应头追加链路追踪
                    .headers(headers -> headers
                                    .addHeaderWriter((req, res) -> res.addHeader("X-Trace-Id", MDC.get("traceId")))
                                    .xssProtection()
//                            .and().cacheControl() // 启用缓存
                    )
//                    .cacheControl().and()
                    //用户访问未经授权的rest API，返回错误码401（未经授权）
                    .exceptionHandling().authenticationEntryPoint(authHandler).accessDeniedHandler(authHandler)
//                    // 指定会话策略；ALWAYS:总是创建HttpSession, IF_REQUIRED:只会在需要时创建一个HttpSession, NEVER:不会创建HttpSession，但如果它已经存在，将可以使用HttpSession, STATELESS:永远不会创建HttpSession，它不会使用HttpSession来获取SecurityContext
                    .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 基于Token，不需要Session
                    .and().authorizeRequests()
                    .antMatchers(HttpMethod.TRACE, "/**").denyAll()
                    // 所有方法都需要登录认证
                    .anyRequest().authenticated()
                    // TODO 建议在类头部或方法头上加权限注解，这里配置太多的权限容易混淆
//                    .antMatchers("/").permitAll()
//                    .antMatchers("/error").permitAll() // HomeController 中需要异常处理方法
                    // 开启 Basic 认证
                    .and().httpBasic()
                    .and().formLogin()
                    .successHandler(authHandler)
                    .failureHandler(authHandler)
            ;
            {
                // 配置退出参数
                http.logout()
                        .logoutSuccessHandler(authHandler)
                        .invalidateHttpSession(true) // 指定是否在注销时让HttpSession无效, 默认设置为 true
                        .deleteCookies("JSESSIONID") // 删除 JSESSIONID
                        .addLogoutHandler(new SecurityContextLogoutHandler()) // 添加一个LogoutHandler, 清除 session
                        // 向客户端发送 清除 “cookie、storage、缓存” 消息
                        //.addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(ClearSiteDataHeaderWriter.Directive.ALL)))
                        .addLogoutHandler((request, response, authentication) -> {// 添加一个LogoutHandler, 退出操作需要删除 redis token 缓存
                            final String token = request.getHeader("x-token");
                            if (StringUtils.isNotBlank(token) && tokenRegexp(token)) {
                                try {
                                    TokenCache.auth(token).destroy();
                                } catch (Exception e) {
                                    log.error(e.getMessage(), e);
                                    // 有可能 token 已经过期了， 所以需要捕获异常，不向前端抛出
                                }
                            }
                        })
                ;
            }
            { // 自定义登录验证适配器验证
//                httpSecurity.authenticationProvider(new AuthenticationProvider());
            }
            { // 自定义过滤器
//                http.addFilterBefore(
//                        new JsonUsernamePasswordAuthenticationFilter(authenticationManager(), authHandler, authHandler),
//                        UsernamePasswordAuthenticationFilter.class
//                );
                // 链路追踪过滤器注册到 Spring Security 过滤器前面； ChannelProcessingFilter.class, SecurityContextPersistenceFilter.class
//                http.addFilterBefore(new RequestIdFilter(), ChannelProcessingFilter.class);
                // token 模式
                http.addFilterAfter(authTokenFilter, BasicAuthenticationFilter.class);
            }
        }

//        /**
//         * 自定义认证方式
//         * @param auth AuthenticationManagerBuilder
//         * @throws Exception 初始化异常
//         */
//        @Override
//        protected void configure(final AuthenticationManagerBuilder builder) throws Exception {
//            final DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
//            provider.setHideUserNotFoundExceptions(false);
//            provider.setUserDetailsService(this.userDetailsService);
//            provider.setPasswordEncoder(passwordEncoder());
//            builder.authenticationProvider(provider);
////          builder.userDetailsService(this.userDetailsService).passwordEncoder(passwordEncoder());
//        }

        /**
         * token 验证模式
         */
        private final OncePerRequestFilter authTokenFilter = new OncePerRequestFilter() {

            @Override
            protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
                final String token = request.getHeader("x-token");
                if (StringUtils.isNotBlank(token)) {
                    try {
                        if (!tokenRegexp(token)) {
                            throw new IllegalArgumentException("token格式错误");
                        }
                        final TabUser user = TokenCache.auth(token).getUser();
                        SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()));
//                    Optional.ofNullable(request.getSession(false)).ifPresent(session -> {
//                        session.setMaxInactiveInterval(1); // 兼容默认的 session 模式，禁止 token 模式创建 session，设置 session 超时时间为1s
//                    });
                    } catch (Exception e) {
                        if (e instanceof CodeException) {
                            log.warn("{}:{}", token, e.getMessage());
                        } else {
                            log.error("{}", token, e);
                        }
                        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "会话超时");
                        return;
//                    response.setContentType(ContentType.json.utf8());
//                    @Cleanup final PrintWriter writer = response.getWriter();
//                    writer.write(new Result<Void>(Code.E00000).json());
//                    writer.flush();
                    }
                }
                filterChain.doFilter(request, response);
            }
        };

        /**
         * 开启密码加密传输
         *
         * @return {@link PasswordEncoder}
         */
        @ConditionalOnExpression("'true'.equals('${spring.app.encrypt.pwd.enabled}') && 'rsa'.equals('${spring.app.encrypt.pwd.type}')")
        @Bean
        public PasswordEncoder rsaBCryptPasswordEncoder() {
            return new BCryptPasswordEncoder() {
                @Override
                public boolean matches(CharSequence rawPassword, String encodedPassword) {
                    // rsa 解密之后再验证密码
                    final String decryptPassword = Rsa.decryptByPrivateKey(rawPassword.toString(), appConfig.getEncrypt().getPwd().getPrivateKey());
                    return super.matches(decryptPassword, encodedPassword);
                }
            };
        }
    }

}
